package com.squareup.anvil.compiler.codegen.incremental

import com.rickbusarow.kase.stdlib.createSafely
import com.rickbusarow.kase.stdlib.div
import io.kotest.matchers.file.shouldExist
import org.junit.Test

internal class FileCacheOperationsTest : CacheTests {

  @Test
  fun `cached bindings are preserved when reading the cache from a file`() = test {

    fileOperations.addToCache(
      gen1.withSources(source1),
      gen2.withSources(source2),
    )

    val newCache = newCache()

    newCache.rootSourceFiles shouldBe setOf(source1, source2)

    newCache.getGeneratedFilesRecursive(source1) shouldBe setOf(gen1)
    newCache.getGeneratedFilesRecursive(source2) shouldBe setOf(gen2)
  }

  @Test
  fun `restoreFromCache passing a source file as input deletes it and everything downstream`() =
    test {

      fileOperations.addToCache(
        gen1.withSources(source1),
        gen2.withSources(source2),
        gen3.withSources(gen2.absolute),
        gen4.withSources(source3),
      )

      listOf(gen1, gen2, gen3)
        .forEach { it.file.delete() }

      fileOperations.restoreFromCache(generatedDir, setOf(source2))

      currentFiles() shouldBe listOf(
        gen1,
        gen4,
        source1,
        source2,
        source3,
      )
    }

  @Test
  fun `restoreFromCache missing generated files are restored`() = test {

    fileOperations.addToCache(
      gen1.withSources(source1),
      gen2.withSources(source2),
      gen3.withSources(gen2.absolute),
    )

    listOf(gen1, gen2, gen3)
      .forEach { it.file.delete() }

    fileOperations.restoreFromCache()

    currentFiles() shouldBe listOf(
      gen1,
      gen2,
      gen3,
      source1,
      source2,
    )

    source1.absoluteFile shouldExistWithText "content for source1.kt"
    source2.absoluteFile shouldExistWithText "content for source2.kt"

    gen1.file shouldExistWithText gen1.content
    gen2.file shouldExistWithText gen2.content
    gen3.file shouldExistWithText gen3.content
  }

  @Test
  fun `restoreFromCache restores generated files with generated sources`() = test {

    // The Gradle plugin passes the source file argument by grabbing all source files from the
    // source set, so for an incremental build that can include generated files.

    fileOperations.addToCache(
      gen1.withSources(source1),
      gen2.withSources(source2),
      gen3.withSources(gen2.absolute),
    )

    fileOperations.restoreFromCache()

    currentFiles() shouldBe listOf(
      gen1,
      gen2,
      gen3,
      source1,
      source2,
    )
  }

  @Test
  fun `restoreFromCache deletes a generated file with no source files when any source changes`() =
    test {

      fileOperations.addToCache(
        gen1,
        gen2.withSources(source1, source2),
        gen3.withSources(source2),
      )

      source1.writeText("changed")

      fileOperations.restoreFromCache(source1)

      currentFiles() shouldBe listOf(gen3, source1, source2)
    }

  @Test
  fun `restoreFromCache deletes the files generated by a changed source file`() = test {

    fileOperations.addToCache(
      gen1.withSources(source1),
      gen2.withSources(source2),
      gen3.withSources(gen2.absolute),
    )

    source2.writeText("changed")

    fileOperations.restoreFromCache(source2)

    currentFiles() shouldBe listOf(gen1, source1, source2)
  }

  @Test
  fun `restoreFromCache deletes any un-tracked files in the generated directory`() = test {

    fileOperations.addToCache(
      gen1.withSources(source1),
    )

    val untracked = generatedDir.resolve("untracked.kt")
      .createSafely("// this is not tracked")
      .let(::AbsoluteFile)

    currentFiles() shouldBe listOf(gen1, untracked, source1)

    fileOperations.restoreFromCache()

    currentFiles() shouldBe listOf(gen1, source1)
  }

  @Test
  fun `restoreFromCache a deleted source file deletes the generated file`() = test {

    fileOperations.addToCache(
      gen1.withSources(source1),
      gen2.withSources(source2),
      gen3.withSources(gen2.absolute),
    )

    listOf(
      source1,
      gen1.absolute,
      gen2.absolute,
      gen3.absolute,
    )
      .forEach { it.absoluteFile.delete() }

    fileOperations.restoreFromCache()

    currentFiles() shouldBe listOf(gen2, gen3, source2)
  }

  @Test
  fun `full restoration works when the position of the build dir relative to the project dir has changed`() =
    test {

      run {
        val projectA = BaseDir.ProjectDir(workingDir / "projectA")
        val buildA = BaseDir.BuildDir(projectA.file / "build")
        val generatedA = buildA.resolve("generated").file

        val operations = FileCacheOperations(
          cache = GeneratedFileCache.fromFile(
            binaryFile = binaryFile,
            projectDir = projectA,
            buildDir = buildA,
          ),
        )

        val source1 by projectA.sourceFile()
        val source2 by projectA.sourceFile()

        val gen1 by generatedA.generatedFile(source1)
        val gen2 by generatedA.generatedFile(source2)
        val gen3 by generatedA.generatedFile(gen2.absolute)

        operations.addToCache(source1, source2, gen1, gen2, gen3)
      }

      val projectB = BaseDir.ProjectDir(workingDir / "projectB")
      val buildB = BaseDir.BuildDir(workingDir / "projectB-build")
      val generatedB = buildB.resolve("generated").file

      val source1 by projectB.sourceFile()
      val source2 by projectB.sourceFile()

      val operations = FileCacheOperations(
        cache = GeneratedFileCache.fromFile(
          binaryFile = binaryFile,
          projectDir = projectB,
          buildDir = buildB,
        ),
      )

      source1.file.shouldExist()
      source2.file.shouldExist()

      operations.restoreFromCache()

      projectB.currentFiles() shouldBe listOf(source1, source2)

      buildB.currentFiles() shouldBe listOf("gen1", "gen2", "gen3")
        .map { AbsoluteFile(generatedB.resolve("$it.kt")) }
    }

  @Test
  fun `incremental restoration works when the position of the build dir relative to the project dir has changed`() =
    test {

      run {
        val projectA = BaseDir.ProjectDir(workingDir / "projectA")
        val buildA = BaseDir.BuildDir(projectA.file / "build")
        val generatedA = buildA.resolve("generated").file

        val operations = FileCacheOperations(
          cache = GeneratedFileCache.fromFile(
            binaryFile = binaryFile,
            projectDir = projectA,
            buildDir = buildA,
          ),
        )

        val source1 by projectA.sourceFile()
        val source2 by projectA.sourceFile()

        val gen1 by generatedA.generatedFile(source1)
        val gen2 by generatedA.generatedFile(source2)
        val gen3 by generatedA.generatedFile(gen2.absolute)

        operations.addToCache(source1, source2, gen1, gen2, gen3)
      }

      val projectB = BaseDir.ProjectDir(workingDir / "projectB")
      val buildB = BaseDir.BuildDir(workingDir / "projectB-build")
      val generatedB = buildB.resolve("generated").file

      val source1 by projectB.sourceFile()
      val source2 by projectB.sourceFile()

      val operations = FileCacheOperations(
        cache = GeneratedFileCache.fromFile(
          binaryFile = binaryFile,
          projectDir = projectB,
          buildDir = buildB,
        ),
      )

      source1.file.shouldExist()
      source2.file.shouldExist()

      operations.restoreFromCache(source2)

      projectB.currentFiles() shouldBe listOf(source1, source2)

      buildB.currentFiles() shouldBe listOf("gen1")
        .map { AbsoluteFile(generatedB.resolve("$it.kt")) }
    }
}
